/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package java.beans;

import java.lang.ref.Reference;
import java.lang.reflect.Method;

/**
 * An IndexedPropertyDescriptor describes a property that acts like an
 * array and has an indexed read and/or indexed write method to access
 * specific elements of the array.
 * <p>
 * An indexed property may also provide simple non-indexed read and write
 * methods.  If these are present, they read and write arrays of the type
 * returned by the indexed read method.
 */

public class IndexedPropertyDescriptor extends PropertyDescriptor {

  private Reference<? extends Class<?>> indexedPropertyTypeRef;
  private final MethodRef indexedReadMethodRef = new MethodRef();
  private final MethodRef indexedWriteMethodRef = new MethodRef();

  private String indexedReadMethodName;
  private String indexedWriteMethodName;

  /**
   * This constructor constructs an IndexedPropertyDescriptor for a property
   * that follows the standard Java conventions by having getFoo and setFoo
   * accessor methods, for both indexed access and array access.
   * <p>
   * Thus if the argument name is "fred", it will assume that there
   * is an indexed reader method "getFred", a non-indexed (array) reader
   * method also called "getFred", an indexed writer method "setFred",
   * and finally a non-indexed writer method "setFred".
   *
   * @param propertyName The programmatic name of the property.
   * @param beanClass The Class object for the target bean.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public IndexedPropertyDescriptor(String propertyName, Class<?> beanClass)
      throws IntrospectionException {
    this(propertyName, beanClass,
        Introspector.GET_PREFIX + NameGenerator.capitalize(propertyName),
        Introspector.SET_PREFIX + NameGenerator.capitalize(propertyName),
        Introspector.GET_PREFIX + NameGenerator.capitalize(propertyName),
        Introspector.SET_PREFIX + NameGenerator.capitalize(propertyName));
  }

  /**
   * This constructor takes the name of a simple property, and method
   * names for reading and writing the property, both indexed
   * and non-indexed.
   *
   * @param propertyName The programmatic name of the property.
   * @param beanClass The Class object for the target bean.
   * @param readMethodName The name of the method used for reading the property values as an array.
   * May be null if the property is write-only or must be indexed.
   * @param writeMethodName The name of the method used for writing the property values as an array.
   * May be null if the property is read-only or must be indexed.
   * @param indexedReadMethodName The name of the method used for reading an indexed property value.
   * May be null if the property is write-only.
   * @param indexedWriteMethodName The name of the method used for writing an indexed property
   * value. May be null if the property is read-only.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public IndexedPropertyDescriptor(String propertyName, Class<?> beanClass,
      String readMethodName, String writeMethodName,
      String indexedReadMethodName, String indexedWriteMethodName)
      throws IntrospectionException {
    super(propertyName, beanClass, readMethodName, writeMethodName);

    this.indexedReadMethodName = indexedReadMethodName;
    if (indexedReadMethodName != null && getIndexedReadMethod() == null) {
      throw new IntrospectionException("Method not found: " + indexedReadMethodName);
    }

    this.indexedWriteMethodName = indexedWriteMethodName;
    if (indexedWriteMethodName != null && getIndexedWriteMethod() == null) {
      throw new IntrospectionException("Method not found: " + indexedWriteMethodName);
    }
    // Implemented only for type checking.
    findIndexedPropertyType(getIndexedReadMethod(), getIndexedWriteMethod());
  }

  /**
   * This constructor takes the name of a simple property, and Method
   * objects for reading and writing the property.
   *
   * @param propertyName The programmatic name of the property.
   * @param readMethod The method used for reading the property values as an array. May be null if
   * the property is write-only or must be indexed.
   * @param writeMethod The method used for writing the property values as an array. May be null if
   * the property is read-only or must be indexed.
   * @param indexedReadMethod The method used for reading an indexed property value. May be null if
   * the property is write-only.
   * @param indexedWriteMethod The method used for writing an indexed property value. May be null if
   * the property is read-only.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public IndexedPropertyDescriptor(String propertyName, Method readMethod, Method writeMethod,
      Method indexedReadMethod, Method indexedWriteMethod)
      throws IntrospectionException {
    super(propertyName, readMethod, writeMethod);

    setIndexedReadMethod0(indexedReadMethod);
    setIndexedWriteMethod0(indexedWriteMethod);

    // Type checking
    setIndexedPropertyType(findIndexedPropertyType(indexedReadMethod, indexedWriteMethod));
  }

  /**
   * Creates <code>PropertyDescriptor</code> for the specified bean
   * with the specified name and methods to read/write the property value.
   *
   * @param bean the type of the target bean
   * @param base the base name of the property (the rest of the method name)
   * @param read the method used for reading the property value
   * @param write the method used for writing the property value
   * @param readIndexed the method used for reading an indexed property value
   * @param writeIndexed the method used for writing an indexed property value
   * @throws IntrospectionException if an exception occurs during introspection
   * @since 1.7
   */
  IndexedPropertyDescriptor(Class<?> bean, String base, Method read, Method write,
      Method readIndexed, Method writeIndexed) throws IntrospectionException {
    super(bean, base, read, write);

    setIndexedReadMethod0(readIndexed);
    setIndexedWriteMethod0(writeIndexed);

    // Type checking
    setIndexedPropertyType(findIndexedPropertyType(readIndexed, writeIndexed));
  }

  /**
   * Gets the method that should be used to read an indexed
   * property value.
   *
   * @return The method that should be used to read an indexed property value. May return null if
   * the property isn't indexed or is write-only.
   */
  public synchronized Method getIndexedReadMethod() {
    Method indexedReadMethod = this.indexedReadMethodRef.get();
    if (indexedReadMethod == null) {
      Class<?> cls = getClass0();
      if (cls == null ||
          (indexedReadMethodName == null && !this.indexedReadMethodRef.isSet())) {
        // the Indexed readMethod was explicitly set to null.
        return null;
      }
      String nextMethodName = Introspector.GET_PREFIX + getBaseName();
      if (indexedReadMethodName == null) {
        Class<?> type = getIndexedPropertyType0();
        if (type == boolean.class || type == null) {
          indexedReadMethodName = Introspector.IS_PREFIX + getBaseName();
        } else {
          indexedReadMethodName = nextMethodName;
        }
      }

      Class<?>[] args = {int.class};
      indexedReadMethod = Introspector.findMethod(cls, indexedReadMethodName, 1, args);
      if ((indexedReadMethod == null) && !indexedReadMethodName.equals(nextMethodName)) {
        // no "is" method, so look for a "get" method.
        indexedReadMethodName = nextMethodName;
        indexedReadMethod = Introspector.findMethod(cls, indexedReadMethodName, 1, args);
      }
      setIndexedReadMethod0(indexedReadMethod);
    }
    return indexedReadMethod;
  }

  /**
   * Sets the method that should be used to read an indexed property value.
   *
   * @param readMethod The new indexed read method.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public synchronized void setIndexedReadMethod(Method readMethod)
      throws IntrospectionException {

    // the indexed property type is set by the reader.
    setIndexedPropertyType(findIndexedPropertyType(readMethod,
        this.indexedWriteMethodRef.get()));
    setIndexedReadMethod0(readMethod);
  }

  private void setIndexedReadMethod0(Method readMethod) {
    this.indexedReadMethodRef.set(readMethod);
    if (readMethod == null) {
      indexedReadMethodName = null;
      return;
    }
    setClass0(readMethod.getDeclaringClass());

    indexedReadMethodName = readMethod.getName();
    setTransient(readMethod.getAnnotation(Transient.class));
  }


  /**
   * Gets the method that should be used to write an indexed property value.
   *
   * @return The method that should be used to write an indexed property value. May return null if
   * the property isn't indexed or is read-only.
   */
  public synchronized Method getIndexedWriteMethod() {
    Method indexedWriteMethod = this.indexedWriteMethodRef.get();
    if (indexedWriteMethod == null) {
      Class<?> cls = getClass0();
      if (cls == null ||
          (indexedWriteMethodName == null && !this.indexedWriteMethodRef.isSet())) {
        // the Indexed writeMethod was explicitly set to null.
        return null;
      }

      // We need the indexed type to ensure that we get the correct method.
      // Cannot use the getIndexedPropertyType method since that could
      // result in an infinite loop.
      Class<?> type = getIndexedPropertyType0();
      if (type == null) {
        try {
          type = findIndexedPropertyType(getIndexedReadMethod(), null);
          setIndexedPropertyType(type);
        } catch (IntrospectionException ex) {
          // Set iprop type to be the classic type
          Class<?> propType = getPropertyType();
          if (propType.isArray()) {
            type = propType.getComponentType();
          }
        }
      }

      if (indexedWriteMethodName == null) {
        indexedWriteMethodName = Introspector.SET_PREFIX + getBaseName();
      }

      Class<?>[] args = (type == null) ? null : new Class<?>[]{int.class, type};
      indexedWriteMethod = Introspector.findMethod(cls, indexedWriteMethodName, 2, args);
      if (indexedWriteMethod != null) {
        if (!indexedWriteMethod.getReturnType().equals(void.class)) {
          indexedWriteMethod = null;
        }
      }
      setIndexedWriteMethod0(indexedWriteMethod);
    }
    return indexedWriteMethod;
  }

  /**
   * Sets the method that should be used to write an indexed property value.
   *
   * @param writeMethod The new indexed write method.
   * @throws IntrospectionException if an exception occurs during introspection.
   */
  public synchronized void setIndexedWriteMethod(Method writeMethod)
      throws IntrospectionException {

    // If the indexed property type has not been set, then set it.
    Class<?> type = findIndexedPropertyType(getIndexedReadMethod(),
        writeMethod);
    setIndexedPropertyType(type);
    setIndexedWriteMethod0(writeMethod);
  }

  private void setIndexedWriteMethod0(Method writeMethod) {
    this.indexedWriteMethodRef.set(writeMethod);
    if (writeMethod == null) {
      indexedWriteMethodName = null;
      return;
    }
    setClass0(writeMethod.getDeclaringClass());

    indexedWriteMethodName = writeMethod.getName();
    setTransient(writeMethod.getAnnotation(Transient.class));
  }

  /**
   * Returns the Java type info for the indexed property.
   * Note that the {@code Class} object may describe
   * primitive Java types such as {@code int}.
   * This type is returned by the indexed read method
   * or is used as the parameter type of the indexed write method.
   *
   * @return the {@code Class} object that represents the Java type info, or {@code null} if the
   * type cannot be determined
   */
  public synchronized Class<?> getIndexedPropertyType() {
    Class<?> type = getIndexedPropertyType0();
    if (type == null) {
      try {
        type = findIndexedPropertyType(getIndexedReadMethod(),
            getIndexedWriteMethod());
        setIndexedPropertyType(type);
      } catch (IntrospectionException ex) {
        // fall
      }
    }
    return type;
  }

  // Private methods which set get/set the Reference objects

  private void setIndexedPropertyType(Class<?> type) {
    this.indexedPropertyTypeRef = getWeakReference(type);
  }

  private Class<?> getIndexedPropertyType0() {
    return (this.indexedPropertyTypeRef != null)
        ? this.indexedPropertyTypeRef.get()
        : null;
  }

  private Class<?> findIndexedPropertyType(Method indexedReadMethod,
      Method indexedWriteMethod)
      throws IntrospectionException {
    Class<?> indexedPropertyType = null;

    if (indexedReadMethod != null) {
      Class params[] = getParameterTypes(getClass0(), indexedReadMethod);
      if (params.length != 1) {
        throw new IntrospectionException("bad indexed read method arg count");
      }
      if (params[0] != Integer.TYPE) {
        throw new IntrospectionException("non int index to indexed read method");
      }
      indexedPropertyType = getReturnType(getClass0(), indexedReadMethod);
      if (indexedPropertyType == Void.TYPE) {
        throw new IntrospectionException("indexed read method returns void");
      }
    }
    if (indexedWriteMethod != null) {
      Class params[] = getParameterTypes(getClass0(), indexedWriteMethod);
      if (params.length != 2) {
        throw new IntrospectionException("bad indexed write method arg count");
      }
      if (params[0] != Integer.TYPE) {
        throw new IntrospectionException("non int index to indexed write method");
      }
      if (indexedPropertyType == null || params[1].isAssignableFrom(indexedPropertyType)) {
        indexedPropertyType = params[1];
      } else if (!indexedPropertyType.isAssignableFrom(params[1])) {
        throw new IntrospectionException(
            "type mismatch between indexed read and indexed write methods: "
                + getName());
      }
    }
    Class<?> propertyType = getPropertyType();
    if (propertyType != null && (!propertyType.isArray() ||
        propertyType.getComponentType() != indexedPropertyType)) {
      throw new IntrospectionException("type mismatch between indexed and non-indexed methods: "
          + getName());
    }
    return indexedPropertyType;
  }

  /**
   * Compares this <code>PropertyDescriptor</code> against the specified object.
   * Returns true if the objects are the same. Two <code>PropertyDescriptor</code>s
   * are the same if the read, write, property types, property editor and
   * flags  are equivalent.
   *
   * @since 1.4
   */
  public boolean equals(Object obj) {
    // Note: This would be identical to PropertyDescriptor but they don't
    // share the same fields.
    if (this == obj) {
      return true;
    }

    if (obj != null && obj instanceof IndexedPropertyDescriptor) {
      IndexedPropertyDescriptor other = (IndexedPropertyDescriptor) obj;
      Method otherIndexedReadMethod = other.getIndexedReadMethod();
      Method otherIndexedWriteMethod = other.getIndexedWriteMethod();

      if (!compareMethods(getIndexedReadMethod(), otherIndexedReadMethod)) {
        return false;
      }

      if (!compareMethods(getIndexedWriteMethod(), otherIndexedWriteMethod)) {
        return false;
      }

      if (getIndexedPropertyType() != other.getIndexedPropertyType()) {
        return false;
      }
      return super.equals(obj);
    }
    return false;
  }

  /**
   * Package-private constructor.
   * Merge two property descriptors.  Where they conflict, give the
   * second argument (y) priority over the first argumnnt (x).
   *
   * @param x The first (lower priority) PropertyDescriptor
   * @param y The second (higher priority) PropertyDescriptor
   */

  IndexedPropertyDescriptor(PropertyDescriptor x, PropertyDescriptor y) {
    super(x, y);
    if (x instanceof IndexedPropertyDescriptor) {
      IndexedPropertyDescriptor ix = (IndexedPropertyDescriptor) x;
      try {
        Method xr = ix.getIndexedReadMethod();
        if (xr != null) {
          setIndexedReadMethod(xr);
        }

        Method xw = ix.getIndexedWriteMethod();
        if (xw != null) {
          setIndexedWriteMethod(xw);
        }
      } catch (IntrospectionException ex) {
        // Should not happen
        throw new AssertionError(ex);
      }
    }
    if (y instanceof IndexedPropertyDescriptor) {
      IndexedPropertyDescriptor iy = (IndexedPropertyDescriptor) y;
      try {
        Method yr = iy.getIndexedReadMethod();
        if (yr != null && yr.getDeclaringClass() == getClass0()) {
          setIndexedReadMethod(yr);
        }

        Method yw = iy.getIndexedWriteMethod();
        if (yw != null && yw.getDeclaringClass() == getClass0()) {
          setIndexedWriteMethod(yw);
        }
      } catch (IntrospectionException ex) {
        // Should not happen
        throw new AssertionError(ex);
      }
    }
  }

  /*
   * Package-private dup constructor
   * This must isolate the new object from any changes to the old object.
   */
  IndexedPropertyDescriptor(IndexedPropertyDescriptor old) {
    super(old);
    this.indexedReadMethodRef.set(old.indexedReadMethodRef.get());
    this.indexedWriteMethodRef.set(old.indexedWriteMethodRef.get());
    indexedPropertyTypeRef = old.indexedPropertyTypeRef;
    indexedWriteMethodName = old.indexedWriteMethodName;
    indexedReadMethodName = old.indexedReadMethodName;
  }

  void updateGenericsFor(Class<?> type) {
    super.updateGenericsFor(type);
    try {
      setIndexedPropertyType(findIndexedPropertyType(this.indexedReadMethodRef.get(),
          this.indexedWriteMethodRef.get()));
    } catch (IntrospectionException exception) {
      setIndexedPropertyType(null);
    }
  }

  /**
   * Returns a hash code value for the object.
   * See {@link java.lang.Object#hashCode} for a complete description.
   *
   * @return a hash code value for this object.
   * @since 1.5
   */
  public int hashCode() {
    int result = super.hashCode();

    result = 37 * result + ((indexedWriteMethodName == null) ? 0 :
        indexedWriteMethodName.hashCode());
    result = 37 * result + ((indexedReadMethodName == null) ? 0 :
        indexedReadMethodName.hashCode());
    result = 37 * result + ((getIndexedPropertyType() == null) ? 0 :
        getIndexedPropertyType().hashCode());

    return result;
  }

  void appendTo(StringBuilder sb) {
    super.appendTo(sb);
    appendTo(sb, "indexedPropertyType", this.indexedPropertyTypeRef);
    appendTo(sb, "indexedReadMethod", this.indexedReadMethodRef.get());
    appendTo(sb, "indexedWriteMethod", this.indexedWriteMethodRef.get());
  }
}
